﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;

namespace RayDen.Library.Components.HashGrids
{
    public class GenericHashGrid<TDataElement>
    {
        public static float NormalThreshold = 0.01f;
        public static float Radius = 0.0001f;
        public static float CellSizeScale = 1.0f;

        //2f / 3f;

        public static int MaxPhotonsPerCell = 1;
        public List<HashGridElement<TDataElement>> hitPoints;

        public int[][] hashGrid;

        protected uint hashGridSize;
        protected int photonsCount;
        protected int maxPhotons;
        protected float invCellSize;
        public int HashGridSize;
        public AABB hpbbox;

        public ulong PhotonsCount
        {
            get
            {
                return (ulong)this.photonsCount;
            }
        }


        public GenericHashGrid( int max, int hsSize = -1 )
        {
            this.maxPhotons = max;
            this.hitPoints = Enumerable.Repeat(new HashGridElement<TDataElement>(), max).ToList();
            this.photonsCount = 0;
            if (hsSize > 0)
            {
                this.HashGridSize = hsSize;
            }
        }


        public void QueryMap( ref Point pos, float maxDist, int nphotons, out HashGridElement<TDataElement>[] result )
        {

            var ix = this.hashGrid[(int)this.Hash(ref pos)];
            if (ix == null)
            {
                result = new HashGridElement<TDataElement>[0];
                return;
            }

            result = new HashGridElement<TDataElement>[Math.Min(nphotons, ix.Length)];


            for (int i = 0; i < Math.Min(nphotons, ix.Length); i++)
            {
                result[i] = this.hitPoints[ix[i]];
            }
        }

        public void Store( ref Point position, TDataElement element )
        {
            if (this.photonsCount > (this.maxPhotons - 2))
                return;
            this.hitPoints[this.photonsCount] = new HashGridElement<TDataElement>
                {
                    Position = position,
                    Data = element
                };

            this.photonsCount++;
        }


        public void ReHash()
        {
            if (this.hitPoints.Count == 0)
                return;
            Tracer.TraceLine("Rehashing...");
            var hitPointsCount = this.hitPoints.Count;
            this.hpbbox = new AABB();
            this.hpbbox.Reset();
            for (int n = 0; n < this.hitPoints.Count; n++)
            {
                this.hpbbox.fit(this.hitPoints[n].Position);

            }
            Vector ssize = this.hpbbox.Size;
            float photonRadius1 = Math.Max(Radius,((ssize.x + ssize.y + ssize.z) / 3.0f) / (ssize.Length2()));

            // Expand the bounding box by used radius
            this.hpbbox.Expand(photonRadius1);

            // Initialize hit points field
            float photonRadius2 = photonRadius1 * photonRadius1;

            // Calculate the size of the grid cell
            float maxPhotonRadius2 = 0f;
            for (int i = 0; i < hitPointsCount; ++i)
            {
                var hp = this.hitPoints[i];

                //if (hp.type == HitPointType.SURFACE)
                maxPhotonRadius2 = Math.Max(maxPhotonRadius2, photonRadius2);
            }

            float cellSize = MathLab.Sqrt(maxPhotonRadius2) * CellSizeScale;
            //std::cerr << "Hash grid cell size: " << cellSize <<std::endl;
            this.invCellSize = 1f / cellSize;
            if (float.IsInfinity(this.invCellSize))
                throw new ArgumentException("Cell size is invalid");

            // TODO: add a tunable parameter for hashgrid size
            this.hashGridSize = (uint)hitPointsCount;


            var hg = Enumerable.Repeat<List<int>>(null, 10000).ToList();

            //for (var i = 0; i < this.hashGridSize; ++i)this.hashGrid.Add(null);

            //std::cerr << "Building hit points hash grid:" << std::endl;
            //std::cerr << "  0k/" << hitPointsCount / 1000 << "k" <<std::endl;
            //unsigned int maxPathCount = 0;
            //double lastPrintTime = WallClockTime();
            ulong entryCount = 0;

            Console.Write("Building grid [");
            var hppMin = (Vector)this.hpbbox.Min;

            for (var i = 0; i < hitPointsCount; ++i)
            {
                //if (WallClockTime() - lastPrintTime > 2.0) {
                //    std::cerr << "  " << i / 1000 << "k/" << hitPointsCount / 1000 << "k" <<std::endl;
                //    lastPrintTime = WallClockTime();
                //}

                var hp = this.hitPoints[i];
                this.hpbbox.fit(this.hitPoints[i].Position - photonRadius1);
                this.hpbbox.fit(this.hitPoints[i].Position + photonRadius1);
                {
                    float photonRadius = MathLab.Sqrt(maxPhotonRadius2);
                    Vector rad = new Vector(photonRadius, photonRadius, photonRadius);
                    Vector bMin = ((hp.Position - rad) - hppMin) * this.invCellSize;
                    Vector bMax = ((hp.Position + rad) - hppMin) * this.invCellSize;


                    var hgl = new List<int>(MaxPhotonsPerCell);
                    hgl.Add(i);
                    for (int iz = Math.Abs((int)bMin.z); iz <= Math.Abs((int)bMax.z); iz++)
                    {
                        for (int iy = Math.Abs((int)bMin.y); iy <= Math.Abs((int)bMax.y); iy++)
                        {
                            for (int ix = Math.Abs((int)bMin.x); ix <= Math.Abs((int)bMax.x); ix++)
                            {
                                int hv = (int)this.Hash(ix, iy, iz);

                                if (hg.Count < hv)
                                {
                                    hg.AddRange(Enumerable.Repeat<List<int>>(null, hv + 1));
                                }

                                if (hg[hv] == null)
                                {
                                    hg[hv] = hgl;
                                }
                                else
                                {
                                    hg[hv].AddRange(hgl);
                                }

                                //new List<int>(MaxPhotonsPerCell);

                                if (hg[hv].Count > MaxPhotonsPerCell)
                                {
                                    MaxPhotonsPerCell = hg[hv].Count;
                                }

                                //hg[hv].Add(i);

                                ++entryCount;



                                /*// hashGrid[hv].size() is very slow to execute
                                if (hashGrid[hv].size() > maxPathCount)
                                    maxPathCount = hashGrid[hv].size();*/
                            }
                        }
                    }

                    if ((i % 100) == 0)
                    {
                        Console.Write("*");
                    }
                }
            }
            Console.Write("]. Complete");
            Tracer.TraceLine();
            //std::cerr << "Max. hit points in a single hash grid entry: " << maxPathCount << std::endl;
            //std::cerr << "Total hash grid entry: " << entryCount << std::endl;
            //std::cerr << "Avg. hit points in a single hash grid entry: " << entryCount / hashGridSize << std::endl;

            // HashGrid debug code
            /*for (unsigned int i = 0; i < hashGridSize; ++i) {
                if (hashGrid[i]) {
                    if (hashGrid[i].size() > 10) {
                        std::cerr << "HashGrid[" << i << "].size() = " <<hashGrid[i].size() << std::endl;
                    }
                }
            }*/
            //var hgc = hg.Where(item=>item != null).Sum(item => item.Count);
            var hgc = this.PhotonsCount;
            Tracer.TraceLine("Total Photons {0} Item Size {1} Total Size {2:F1} KBytes ", this.hitPoints.Count, Marshal.SizeOf(typeof(TDataElement))+12, this.hitPoints.Count * (12+Marshal.SizeOf(typeof(TDataElement))) / 1000f);
            Tracer.TraceLine("Total Items in Grid {0} Item Size {1} Total Size {2:F1} KBytes ", hgc, 4, (hgc * 4f) / 1000f);
            this.hashGrid = hg.Select(item => item != null ? item.ToArray() : null).ToArray();
            GC.Collect();
            Tracer.TraceLine("Complete...");
        }

        protected uint Hash( int ix, int iy, int iz )
        {
            return ((uint)((ix * 73856093U) ^ (iy * 19349663U) ^ (iz * 83492791U)) % this.hashGridSize);
        }

        private uint Hash( ref Vector i )
        {
            return ((((uint)i.x * 73856093U) ^ ((uint)i.y * 19349663U) ^ ((uint)i.z * 83492791U)) % this.hashGridSize);
        }

        protected uint Hash( ref Point i )
        {
            return ((((uint)i.x * 73856093U) ^ ((uint)i.y * 19349663U) ^ ((uint)i.z * 83492791U)) % this.hashGridSize);
        }
        private uint Hash( Vector i )
        {
            return ((((uint)i.x * 73856093U) ^ ((uint)i.y * 19349663U) ^ ((uint)i.z * 83492791U)) % this.hashGridSize);
        }


        #region IPhotonMap Members

        public TDataElement[] GetPlainData()
        {
            return this.hitPoints.Select(item=>item.Data).ToArray();
        }

        public virtual void ScalePhotonMap( float scale )
        {
        }

        public void SavePhotonMap( string fileName )
        {

        }

        public void LoadPhotonMap( string fileName )
        {

        }

        internal bool hashed = false;
        public void Balance()
        {
            try
            {
                this.ReHash();
                return;

                /*if (!this.hashed) {
                    this.ReHash();
                    this.hashed = true;
                }*/
            }
            catch (Exception ex)
            {
                Tracer.TraceLine(ex.StackTrace);
            }
        }

        #endregion
    }
}